
import re
import os
import os.path

import pygame
from pygame.locals import *

from resource import Resource
from rule import Rule, Event

from cache import SimpleLRUCache

class CachedResource(Resource):
	def __init__(self, **params):
		Resource.__init__(self, **params)
		self.images = SimpleLRUCache(5)
		self.sounds = SimpleLRUCache(5)

	def lstream(self, path, callback):
		return self.call_resource_as_stream(path, callback)

	def load_image(self, file):
		cached = self.images[file]
		if cached:
			return cached
		callback = pygame.image.load
		image = self.lstream(file+'.tlg', callback) or self.lstream(file+'.png', callback)
		self.images[file] = image
		return image

	def load_sound(self, file):
		cached = self.sounds[file]
		if cached:
			# XXX: if the sound is already in play ...
			return cached
		callback = pygame.mixer.Sound
		sound = self.lstream(file+'.ogg', callback) or self.lstream(file+'.wav', callback) or DummySound()
		if sound:
			self.sounds[file] = sound
		return sound

def image_rect(x):
	if x:
		return pygame.Rect(x[1][0], x[1][1], x[0].get_width(), x[0].get_height())

def merge_rects(rects):
	rect = None
	for r in rects:
		if r and not rect:
			rect = r
		elif r:
			rect = rect.union(r)
	return rect


class Background(pygame.Surface):
	def __init__(self, size=(800, 600)):
		self.size = size

		pygame.Surface.__init__(self, size)
		self.convert()
		self.fill((250, 250, 250))

		self.default_bg = pygame.Surface(size)
		self.default_bg.convert()
		self.default_bg.fill((250, 250, 250))
		self.bg = self.default_bg
		self.fg = [None] * 5

		self.rect = pygame.Rect(0, 0, *size)
		self.updated_rect = None

	def image(self, image):
		self.updated_rect = self.rect
		self.bg = image or self.default_bg
		self.blit(self.bg, (0, 0))
		self.updated_rect = self.rect

	def update_rect(self, rect):
		if rect:
			if self.updated_rect:
				self.updated_rect.union_ip(rect)
			else:
				self.updated_rect = rect

	def ldall(self, images):
		rect = merge_rects(map(image_rect, self.fg))

		self.fg = [None] * 5
		for k, image in images.items():
			if k == 'l':
				self.fg[0] = (image, ((self.size[0]/2-image.get_width())/2, self.size[1]-image.get_height()))
			elif k == 'c':
				self.fg[2] = (image, ((self.size[0]-image.get_width())/2, self.size[1]-image.get_height()))
			elif k == 'rc':
				self.fg[3] = (image, ((self.size[0]*2/3-image.get_width()/2), self.size[1]-image.get_height()))
			elif k == 'r':
				self.fg[4] = (image, ((self.size[0]/2-image.get_width())/2+self.size[0]/2, self.size[1]-image.get_height()))
			else:
				print k
				raise NotImplementedError()

		rect = merge_rects([rect, merge_rects(map(image_rect, self.fg))])

		if rect:
			self.blit(self.bg, rect.topleft, rect)

		for i in self.fg:
			if i:
				self.blit(*i)

		self.update_rect(rect)

	def ld(self, image, pos):
		old_fg = None
		new_fg = None
		if pos == 'c' or pos == 'center':
			old_fg = self.fg[:]
			self.fg[2] = (image, ((self.size[0]-image.get_width())/2, self.size[1]-image.get_height()))
			self.fg[0] = self.fg[1] = self.fg[3] = self.fg[4] = None
			new_fg = self.fg[2]
		elif pos == 'l' or pos == 'left':
			old_fg = self.fg[0:4]
			self.fg[0] = (image, ((self.size[0]/2-image.get_width())/2, self.size[1]-image.get_height()))
			self.fg[1] = self.fg[2] = self.fg[3] = None
			new_fg = self.fg[0]
		elif pos == 'r' or pos == 'right':
			old_fg = self.fg[1:]
			self.fg[4] = (image, ((self.size[0]/2-image.get_width())/2+self.size[0]/2, self.size[1]-image.get_height()))
			self.fg[1] = self.fg[2] = self.fg[3] = None
			new_fg = self.fg[4]
		elif pos == 'rc' or pos == 'rightcenter':
			old_fg = self.fg[:]
			self.fg[3] = (image, ((self.size[0]*2/3-image.get_width()/2), self.size[1]-image.get_height()))
			self.fg[0] = self.fg[1] = self.fg[2] = self.fg[4] = None
			new_fg = self.fg[3]
		else:
			print pos
			raise NotImplementedError()

		rect = merge_rects(map(image_rect, old_fg))
		if rect:
			self.blit(self.bg, rect.topleft, rect)
		if new_fg:
			self.blit(*new_fg)
		self.update_rect(rect)
		self.update_rect(image_rect(new_fg))

	def cl(self, pos):
		rect = None
		if pos == 'all':
			rect = merge_rects(map(image_rect, self.fg))
			self.fg = [None] * 5
		elif pos == 'l' or pos == 'left':
			rect = image_rect(self.fg[0])
			self.fg[0] = None
		elif pos == 'c' or pos == 'center':
			rect = image_rect(self.fg[2])
			self.fg[2] = None
		elif pos == 'rc' or pos == 'rightcenter':
			rect = image_rect(self.fg[3])
			self.fg[3] = None
		elif pos == 'r' or pos == 'right':
			rect = image_rect(self.fg[4])
			self.fg[4] = None
		else:
			print pos
			raise NotImplementedError()
		if rect:
			self.blit(self.bg, rect.topleft, rect)
		self.update_rect(rect)

class TextBoard(pygame.Surface):
	def __init__(self, screen):
		screen_size = screen.get_size()
		self.x = 50
		self.y = 50
		self.size = (screen_size[0] - 2 * self.x, screen_size[1] - 2 * self.y)
		self.board_rect = pygame.Rect(self.x, self.y, *self.size)
		self.text_rect = pygame.Rect(self.x+10, self.y+10, self.size[0]-20, self.size[1]-20)

		self.font_color = (255, 255, 255, 0)
		self.bg_color = (0, 0, 0)

		self.screen = screen

		pygame.Surface.__init__(self, self.size)
		self.convert()
		self.set_alpha(147)

		font_name = 'msyh.ttf'
		font_size = 24
		ruby_size = font_size/2
		self.ruby_size = ruby_size
		if os.path.exists(font_name):
			self.font = pygame.font.Font(font_name, font_size)
			self.ruby_font = pygame.font.Font(font_name, ruby_size)
		else:
			fonts = [path for path in (os.path.join(dir, font_name) for dir in [r'C:\Windows\Fonts']) if os.path.exists(path)]
			if fonts:
				self.font = pygame.font.Font(fonts[0], font_size)
				self.ruby_font = pygame.font.Font(fonts[0], ruby_size)
			else:
				raise RuntimeError("Fon't not found")

		self.fill(self.bg_color)
		pygame.draw.rect(self, (255, 255, 255), pygame.Rect(1, 1, self.size[0]-2, self.size[1]-2), 1)
		pygame.draw.rect(self, (255, 255, 255), pygame.Rect(3, 3, self.size[0]-6, self.size[1]-6), 1)
		self.clear()

		self.texton = False
		self.texthidden = False

		self.highlight_bar = pygame.Surface(self.size)
		self.highlight_bar.convert()
		self.highlight_bar.set_alpha(64)
		self.highlight_bar.fill((196, 196, 196))

		self.highlight = None

		self.updated_rect = None

	def display(self, text):
		if text == '[ws canskip=true][lvend]': # XXX: can I simply ignore this?
			return
		text = text.replace('[l]', '')
		text = text.replace('[r]', '')
		text = re.sub(r'\[line(\d+)\]', lambda x: u'\u2014'*int(x.group(1)), text)
		text = text.replace('[szlig]', u'\u00DF') # XXX: any more?

		ruby = self.ruby_parse(text)

		n = len(self.lines)

		if ruby:
			self.display_ruby(ruby)
		else:
			self.display_text(text)

		if text:
			self.updated_rect = merge_rects([self.updated_rect, merge_rects(map(image_rect, self.lines[n:]))])

		return text

	def display_text(self, text):
		text_surface = self.font.render(text, 1, self.font_color)
		line_rect = text_surface.get_rect(top=self.top, left=self.text_rect.left)

		if line_rect.width > self.text_rect.width:
			n = self.text_rect.width * len(text) / line_rect.width

			text_surface = self.font.render(text[:n], 1, self.font_color)
			line_rect = text_surface.get_rect(top=self.top, left=self.text_rect.left)

			self.top = line_rect.bottom
			self.lines.append((text_surface, line_rect))

			self.display_text(text[n:])

		else:
			self.top = line_rect.bottom
			self.lines.append((text_surface, line_rect))

	def ruby_parse(self, text):
		matches = [(m.start(), m.end(), m.group(1)) for m in re.finditer(r'\[ruby text="([^"]*)"]', text)]
		if not matches:
			return
		ruby_starts, ruby_ends, rubies = zip(*matches)

		x = zip(rubies, [text[m[0]:m[1]] for m in zip(ruby_ends, ruby_starts[1:]+(-1,))])
		if ruby_starts[0]:
			x.insert(0, (None, text[:ruby_starts[0]]))

		return x

	def display_ruby_word(self, ruby, text, left):
		if ruby and len(text) > 2:
			left, bottom, rest = self.display_ruby_word(ruby, text[0], left)
			if rest:
				left = self.text_rect.left
				self.top = bottom
				left, bottom, rest = self.display_ruby_word(ruby, text[0], left)
				assert not rest
			return self.display_ruby_word(None, text[1:], left)

		rest = None
		text_surface = self.font.render(text, 1, self.font_color)
		line_rect = text_surface.get_rect(top=self.top+self.ruby_size, left=left)

		if line_rect.right > self.text_rect.right:
			n = (self.text_rect.right - line_rect.left) * len(text) / line_rect.width
			if not n:
				return left, line_rect.bottom, (ruby, text)

			text_surface = self.font.render(text[:n], 1, self.font_color)
			line_rect = text_surface.get_rect(top=self.top+self.ruby_size, left=left)
			rest = (None, text[n:])

		self.lines.append((text_surface, line_rect))

		if ruby:
			ruby_surface = self.ruby_font.render(ruby, 1, self.font_color)
			ruby_rect = ruby_surface.get_rect(top=self.top, left=left)
			if ruby_rect.right > line_rect.right and (ruby.startswith(' ') or ruby.startswith(u'\u3000')):
				ruby_rect.right -= ruby_rect.right - line_rect.right
			elif len(ruby) == 1 and ruby_rect.width < line_rect.width:
				ruby_rect.right += (line_rect.right - ruby_rect.right) / 2
			self.lines.append((ruby_surface, ruby_rect))

		left = line_rect.right
		return left, line_rect.bottom, rest

	def display_ruby(self, rubies):
		left = self.text_rect.left
		bottom = self.top
		for ruby, text in rubies:
			left, bottom, rest = self.display_ruby_word(ruby, text, left)
			while rest:
				# TODO: if the text has multiple liens, check if a certain line has ruby, and if not, don't display an empty ruby row
				self.top = bottom
				left, bottom, rest = self.display_ruby_word(rest[0], rest[1], self.text_rect.left)

		self.top = bottom

	def display_alternatives(self, alternatives):
		self.clear()
		for i in range(len(alternatives)):
			# TODO: handle long alternative
			# TODO: probably don't highlight bullet
			self.display_text(u'%d. %s' % (i+1, alternatives[i]))

	def decide_alternative(self, x, y):
		for i in range(len(self.lines)):
			rect = self.lines[i][1]
			if rect.left < x < rect.right and rect.top < y < rect.bottom:
				return i + 1

	def highlight_alternative(self, x, y):
		last = self.highlight
		n = self.decide_alternative(x, y)
		if n:
			self.highlight = n - 1
		else:
			self.highlight = None
		if last != self.highlight:
			self.updated_rect = self.board_rect

	def on(self):
		if not self.is_visible():
			self.updated_rect = self.board_rect
		self.texton = True
		self.texthidden = False

	def off(self):
		if self.is_visible():
			self.updated_rect = self.board_rect
		self.texton = False
		self.texthidden = False

	def hide(self):
		if self.is_visible():
			self.updated_rect = self.board_rect
		self.texthidden = True

	def show(self):
		if self.is_hidden():
			self.updated_rect = self.board_rect
		self.texthidden = False

	def is_hidden(self):
		return self.texton and self.texthidden

	def is_visible(self):
		return self.texton and not self.texthidden

	def paint(self):
		self.screen.blit(self, (50, 50))
		for line, textpos in self.lines:
			self.screen.blit(line, textpos)
		if self.highlight is not None:
			self.screen.blit(self.highlight_bar, self.lines[self.highlight][1], self.lines[self.highlight][1])

	def clear(self):
		self.top = self.text_rect.top
		self.lines = []
		self.highlight = None
		self.updated_rect = self.board_rect

class DummySound:
	def play(self, loops=None):
		return DummyChannel()
	def stop(self):
		pass

class DummyChannel:
	def get_busy(self):
		return False

class SoundBox:
	def __init__(self, resource):
		self.resource = resource
		self.bgm = None
		self.seloop = None
		self.lvoice = None
		self.lvoice_channel = None

	def play_bgm(self, sound):
		sound = self.resource.load_sound(sound)
		if self.bgm:
			self.bgm.stop()
		self.bgm = sound
		sound.play(loops=365*24*60*60)

	def play_se(self, sound):
		sound = self.resource.load_sound(sound)
		sound.play()

	def play_seloop(self, sound):
		sound = self.resource.load_sound(sound)
		if self.seloop:
			self.seloop.stop()
		self.seloop = sound
		sound.play(loops=365*24*60*60)

	def play_sestop(self):
		if self.seloop:
			self.seloop.stop()

	def play_lvoice(self, sound):
		sound = self.resource.load_sound(sound)
		if self.lvoice:
			self.lvoice.stop()
		self.lvoice = sound
		self.lvoice_channel = sound.play()

	def is_lvoice_playing(self):
		return self.lvoice and self.lvoice_channel and self.lvoice_channel.get_busy()

	def stop_lvoice(self):
		if self.lvoice:
			self.lvoice.stop()
		self.lvoice = None
		self.lvoice_channel = None

class DummySoundBox:
	def __init__(self, resource):
		pass
	def play_bgm(self, sound):
		pass
	def play_se(self, sound):
		pass
	def play_seloop(self, sound):
		pass
	def play_sestop(self):
		pass
	def play_lvoice(self, sound):
		pass
	def is_lvoice_playing(self):
		pass
	def stop_lvoice(self):
		pass

class Game:
	def __init__(self, resource):
		self.resource = resource
		self.rule = Rule(resource)

		self.screen_size = (800, 600)

	def start(self):
		pygame.init()
		pygame.display.set_caption('Code/stay night')
		if os.path.exists('Fate_MAINICON.ico'):
			pygame.display.set_icon(pygame.image.load('Fate_MAINICON.ico'))

		screen = pygame.display.set_mode((800, 600))
		screen_size = screen.get_size()
		buffer = pygame.Surface(screen_size)
		buffer.convert()

		background = Background()

		textboard = TextBoard(buffer)

		sound = DummySoundBox(self.resource) # debug
		sound = SoundBox(self.resource)

		buffer.blit(background, (0, 0))
		screen.blit(buffer, (0, 0))
		pygame.display.flip()

		self.screen = screen
		self.buffer = buffer
		self.background = background
		self.textboard = textboard
		self.sound = sound

		self.paint_now = False

	def paint(self):
		#if not self.paint_now:
		#	return
		bgrect = self.background.updated_rect
		textrect = self.textboard.updated_rect
		if not bgrect and not textrect:
			return

		if not bgrect and textrect != self.textboard.board_rect:
			if self.textboard.is_visible():
				for line, textpos in self.textboard.lines:
					if textrect.contains(textpos):
						self.textboard.screen.blit(line, textpos)
		else:
			if bgrect:
				textrect = self.textboard.board_rect
			if bgrect and textrect:
				rect = bgrect.union(textrect)
			elif textrect:
				rect = textrect
			else:
				rect = bgrect
			if rect:
				self.buffer.blit(self.background, rect.topleft, rect)
			if self.textboard.is_visible():
				self.textboard.paint()

		self.background.updated_rect = None
		self.textboard.updated_rect = None
		self.screen.blit(self.buffer, (0, 0))
		pygame.display.flip()
		self.paint_now = False

	def buffered_paint(self):
		raise NotImplementedError()

	def display_text(self, text):
		#print text
		if self.textboard.display(text):
			self.paint_now = True

	def clear_text(self):
		self.textboard.clear()

	def display_alternatives(self, alternatives):
		self.textboard.display_alternatives(alternatives)
		self.paint_now = True

	def loop(self):

		clock = pygame.time.Clock()

		input = None

		going = True

		selection_mode = False

		from time import time
		start = time()
		i = 0
		while going:

			clock.tick(100) # debug
			i += 0

			for event in pygame.event.get():
				if event.type == QUIT:
					going = False
				elif event.type == KEYDOWN and event.key == K_ESCAPE:
					going = False
				elif event.type == KEYDOWN and event.key == K_BACKSPACE:
					if self.textboard.is_hidden():
						self.textboard.show()
						self.paint_now = True
					elif self.textboard.is_visible():
						self.textboard.hide()
						self.paint_now = True

				elif event.type == KEYDOWN and event.key == ord('\t'):
					self.rule.skip_scene()
					input = 'click'

				elif selection_mode and event.type == MOUSEBUTTONDOWN:
					n = self.textboard.decide_alternative(*event.pos)
					if n:
						self.clear_text()
						input = n
						selection_mode = None
				elif selection_mode and event.type == KEYDOWN:
					if ord('1') <= event.key <= ord('9'):
						n = event.key - ord('1') + 1
						if n <= len(self.textboard.lines):
							self.clear_text()
							input = n
							selection_mode = None
				elif selection_mode and event.type == MOUSEMOTION:
					# TODO: change cursor to hand
					self.textboard.highlight_alternative(*event.pos)
					self.paint_now = True

				elif (event.type == MOUSEBUTTONUP and event.button == 1) or (event.type == KEYDOWN and event.key in (K_SPACE, K_RETURN)):
					if self.textboard.is_hidden():
						self.textboard.show()
					else:
						input = 'click'
			else:
				if pygame.key.get_pressed()[K_LCTRL]:
					if self.textboard.is_hidden():
						self.textboard.show()
					input = 'ctrl_click'
				#input = 'ctrl_click' # debug

			if (input == 'click' or input == 'ctrl_click') and self.sound.is_lvoice_playing():
				self.sound.stop_lvoice()
				event = self.rule.next(None)
			else:
				event = self.rule.next(input)

			if event:
				if event.type == 'text':
					self.display_text(event['text'])
				elif event.type == 'r':
					self.display_text('')
				elif event.type == 'pg' or event.type == 'pgnl':
					self.clear_text()
				elif event.type == 'selecter':
					self.display_alternatives(event['alternative'])
					selection_mode = True
					#going = False # debug

				elif event.type == 'bg' or event.type == 'fadein':
					self.background.image(self.resource.load_image(event['file']))
				elif event.type == 'rep':
					self.background.image(self.resource.load_image(event['bg']))
				elif event.type == 'ld_auto' or event.type == 'ld':
					self.background.ld(self.resource.load_image(event['file']), event['pos'])
				elif event.type == 'ldall':
					images = {}
					for k, v in event.params.items():
						if k == 'l':
							images['l'] = self.resource.load_image(v)
						elif k == 'r':
							images['r'] = self.resource.load_image(v)
						elif k == 'c':
							images['c'] = self.resource.load_image(v)
						elif k == 'method' or k == 'time' or k == 'il' or k == 'ir' or k == 'ic':
							pass
						else:
							raise NotImplementedError, k
					self.background.ldall(images)
				elif event.type == 'cl':
					self.background.cl(event['pos'])
				elif event.type in ('a2aT', 'a2a', 'i2oT', 'i2i', 'i2iT'):
					self.background.image(self.resource.load_image(event['file']))
				elif event.type == 'dashcombo':
					self.background.image(self.resource.load_image(event['storage']))
				# TODO: superpose

				elif event.type == 'play':
					self.sound.play_bgm(event['file'])
				elif event.type == 'se':
					if input != 'ctrl_click':
						self.sound.play_se(event['file'])
				elif event.type == 'seloop':
					if input != 'ctrl_click':
						self.sound.play_seloop(event['file'])
				elif event.type == 'sestop':
					self.sound.play_sestop() # XXX: sound file ignored?
				elif event.type == 'lvoice':
					if input != 'ctrl_click':
						self.sound.play_lvoice(event['file'])

				elif event.type == 'texton':
					self.textboard.on()
				elif event.type == 'textoff':
					self.textboard.off()
				else:
					pass

			input = None
			self.paint()

		print time() - start, i

		pygame.quit()


if __name__ == '__main__':
	game = Game(CachedResource())
	game.start()
	game.loop()

